/*
 * Copyright (C) 2004-2005 Jonathan Bindel
 * Copyright (C) 2006-2007 Eskil Bylund
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

using System;
using System.Diagnostics;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Timers;

using DCSharp.Backend.Protocols;

namespace DCSharp.Backend.Connections
{
	public enum ConnectionState
	{
		Disconnected,
		Connecting,
		Connected
	}

	public enum ConnectionError
	{
		Dns,
		Timeout,
		General
	}

	public enum ConnectionMode
	{
		Message,
		Data
	}

	public class ConnectionEventArgs : EventArgs
	{
		private Connection connection;

		public ConnectionEventArgs(Connection connection)
		{
			this.connection = connection;
		}

		public Connection Connection
		{
			get { return connection; }
		}
	}

	public class ConnectionErrorEventArgs : EventArgs
	{
		private ConnectionError error;

		public ConnectionErrorEventArgs(ConnectionError error)
		{
			this.error = error;
		}

		public ConnectionError Error
		{
			get { return error; }
		}
	}

	/// <remarks>
	/// Implementations must set the Protocol property in the constructor or
	/// by overriding the virtual method ParseData.
	/// </remarks>
	public abstract class Connection : IDisposable
	{
		public event EventHandler StateChanged;
		public event EventHandler<ConnectionErrorEventArgs> Error;

		protected const int BufferSize = 16384;

		private ConnectionState state;
		private ConnectionMode mode;
		private Socket client;
		private IPEndPoint endPoint;
		private IProtocol protocol;
		private bool incoming;

		private Timer connectionTimer;

		private AsyncCallback receivedData;
		private byte[] buffer;
		private StringBuilder messageBuffer;
		private byte messageSeparator;

		private bool disposed;

		#region Constructors

		protected Connection()
		{
			state = ConnectionState.Disconnected;
			mode = ConnectionMode.Message;

			connectionTimer = new Timer();
			connectionTimer.Elapsed += new ElapsedEventHandler(OnTimerElapsed);
			connectionTimer.Interval = 10000;

			buffer = new byte[BufferSize];
			messageBuffer = new StringBuilder();

			receivedData = new AsyncCallback(OnReceivedData);

			if (client == null)
			{
				client = new Socket(AddressFamily.InterNetwork,
					SocketType.Stream, ProtocolType.Tcp);
			}
		}

		#endregion

		#region Properties

		public ConnectionState State
		{
			get { return state; }
			protected set
			{
				if (state != value)
				{
					state = value;
					OnStateChanged();
				}
			}
		}

		public ConnectionMode Mode
		{
			get { return mode; }
			protected set { mode = value; }
		}

		protected Socket Client
		{
			get { return client; }
			set { client = value; }
		}

		protected IPEndPoint EndPoint
		{
			get { return endPoint; }
			set { endPoint = value; }
		}

		protected IProtocol Protocol
		{
			get { return protocol; }
			set
			{
				protocol = value;
				messageSeparator = protocol.Encoding.GetBytes(protocol.MessageSeparator)[0];
			}
		}

		public bool Incoming
		{
			get { return incoming; }
		}

		#endregion

		#region Methods

		#region Connect

		public virtual void Connect()
		{
			CheckDisposed();

			State = ConnectionState.Connecting;

			// Is this an incoming or outgoing connection?
			if (!client.Connected)
			{
				connectionTimer.Start();
				try
				{
					client.BeginConnect(endPoint,
						new AsyncCallback(SocketConnected), client);
				}
				catch (SocketException e)
				{
					OnError(ConnectionError.General, e);
					Disconnect();
				}
			}
			else
			{
				incoming = true;
				HandleSocketConnected();
			}
		}

		private void SocketConnected(IAsyncResult ar)
		{
			Socket client = (Socket)ar.AsyncState;
			try
			{
				client.EndConnect(ar);
				connectionTimer.Stop();

				HandleSocketConnected();
			}
			catch (ObjectDisposedException)
			{
			}
			catch (Exception e)
			{
				OnError(ConnectionError.General, e);
				Disconnect();
			}
		}

		protected virtual void HandleSocketConnected()
		{
			if (protocol != null)
			{
				protocol.Initialize();
			}
			Receive();
		}

		public virtual void Disconnect()
		{
			Dispose();
		}

		internal void SetStateConnected()
		{
			if (State == ConnectionState.Connecting)
			{
				State = ConnectionState.Connected;
			}
		}

		private void OnTimerElapsed(object obj, ElapsedEventArgs args)
		{
			if (State == ConnectionState.Connecting)
			{
				OnError(ConnectionError.Timeout, null);
				Disconnect();
			}
		}

		#endregion

		#region Send

		internal virtual void Send(string message)
		{
			Send(message, protocol.Encoding);
		}

		internal virtual void Send(string message, Encoding encoding)
		{
			if (encoding == null)
			{
				throw new ArgumentNullException("encoding");
			}

			byte[] bytes = encoding.GetBytes(message);
			client.Send(bytes);
		}

		internal virtual void Send(byte[] data)
		{
			Send(data, 0, data.Length);
		}

		internal virtual void Send(byte[] data, int offset, int count)
		{
			client.Send(data, offset, count, SocketFlags.None);
		}

		#endregion

		#region Receive

		protected virtual void Receive()
		{
			try
			{
				client.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None,
					receivedData, client);
			}
			catch (ObjectDisposedException)
			{
			}
			catch (Exception e)
			{
				OnError(ConnectionError.General, e);
				Disconnect();
			}
		}

		private void OnReceivedData(IAsyncResult result)
		{
			int length = 0;
			Socket client = (Socket)result.AsyncState;
			try
			{
				length = client.EndReceive(result);
				if (length > 0)
				{
					ParseData(buffer, 0, length);
					Receive();
				}
				else
				{
					Debug.WriteLine("No more data.");
					Disconnect();
				}
			}
			catch (ObjectDisposedException)
			{
			}
			catch (Exception e)
			{
				OnError(ConnectionError.General, e);
				Disconnect();
			}
		}

		protected virtual void ParseData(byte[] buffer, int offset, int length)
		{
			int pos = offset;
			do
			{
				switch (mode)
				{
					case ConnectionMode.Message:
						int end = Array.IndexOf(buffer, messageSeparator, pos,
							length - pos);
						if (end >= 0)
						{
							string message = protocol.Encoding.GetString(buffer, pos, end - pos);
							if (messageBuffer.Length > 0)
							{
								messageBuffer.Append(message);
								message = messageBuffer.ToString();
								messageBuffer.Remove(0, messageBuffer.Length);
							}
							if (message.Length > 0)
							{
								HandleMessage(message);
							}
							pos = end + 1;
						}
						else
						{
							messageBuffer.Append(protocol.Encoding.GetString(buffer, pos, length - pos));
							pos = length;
						}
						break;

					case ConnectionMode.Data:
						int handled = HandleData(buffer, pos, length - pos);
						pos += handled;
						break;
				}
			}
			while (pos < length && !disposed);
		}

		protected virtual void HandleMessage(string message)
		{
			if (message == null)
			{
				throw new ArgumentNullException("message");
			}
			Protocol.Handle(message);
		}

		protected virtual int HandleData(byte[] buffer, int offset, int count)
		{
			throw new NotSupportedException();
		}

		#endregion

		protected void CheckDisposed()
		{
			if (disposed)
			{
				throw new ObjectDisposedException(GetType().FullName);
			}
		}

		protected virtual void OnError(ConnectionError error, Exception e)
		{
			if (Error != null)
			{
				Error(this, new ConnectionErrorEventArgs(error));
			}
		}

		protected virtual void OnStateChanged()
		{
			if (StateChanged != null)
			{
				StateChanged(this, EventArgs.Empty);
			}
		}

		#region IDisposable Members

		public void Dispose()
		{
			Dispose(true);
			GC.SuppressFinalize(this);
		}

		protected virtual void Dispose(bool disposing)
		{
			if (disposed)
			{
				return;
			}

			disposed = true;
			if (disposing)
			{
				State = ConnectionState.Disconnected;

				client.Close();
				connectionTimer.Dispose();
			}
		}

		#endregion

		#endregion
	}
}
